package com.cgrs572.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cgrs572.shortlink.project.common.convention.exception.ClientException;
import com.cgrs572.shortlink.project.common.convention.exception.ServiceException;
import com.cgrs572.shortlink.project.common.enums.VailDateTypeEnum;
import com.cgrs572.shortlink.project.config.GotoDomainWhiteListConfiguration;
import com.cgrs572.shortlink.project.dao.entity.*;
import com.cgrs572.shortlink.project.dao.mapper.*;
import com.cgrs572.shortlink.project.dto.biz.ShortLinkStatsRecordDTO;
import com.cgrs572.shortlink.project.dto.req.ShortLinkBatchCreateReqDTO;
import com.cgrs572.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.cgrs572.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.cgrs572.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.cgrs572.shortlink.project.dto.resp.*;
import com.cgrs572.shortlink.project.mq.producer.ShortLinkStatsSaveProducer;
import com.cgrs572.shortlink.project.service.LinkStatsTodayService;
import com.cgrs572.shortlink.project.service.ShortLinkService;
import com.cgrs572.shortlink.project.toolkit.HashUtil;
import com.cgrs572.shortlink.project.toolkit.LinkUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.cgrs572.shortlink.project.common.constant.RedisKeyConstant.*;

/**
 * 短链接接口实现曾（业务实现层）
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;// 布隆过滤器
    private final ShortLinkGotoMapper shortLinkGotoMapper; // 用于短链接跳转
    private final StringRedisTemplate stringRedisTemplate; // 利用Java操作Redis
    private final RedissonClient redissonClient; // Redisson 客户端的顶层接口，可用来获取redisson分布式锁或redisson读写锁
    private final LinkAccessStatsMapper linkAccessStatsMapper; // 短链接基础访问监控持久层Bean
    private final LinkLocaleStatsMapper linkLocaleStatsMapper; // 短链接地区统计监控持久层Bean
    private final LinkOsStatsMapper linkOsStatsMapper; // 短链接访问时所用的OS统计持久层Bean
    private final LinkBrowserStatsMapper linkBrowserStatsMapper; // 短链接访问所用的浏览器统计持久层Bean
    private final LinkAccessLogsMapper linkAccessLogsMapper; // 短链接访问日志监控持久层Bean---可用来获取ip地址
    private final LinkDeviceStatsMapper linkDeviceStatsMapper; // 短链接访问设备监控持久层Bean
    private final LinkNetworkStatsMapper linkNetworkStatsMapper; // 短链接访问网络监控持久层Bean
    private final LinkStatsTodayMapper linkStatsTodayMapper; // 短链接今日访问监控持久层Bean
    private final LinkStatsTodayService linkStatsTodayService; // 短链接今日统计接口层（业务层）Bean
    private final GotoDomainWhiteListConfiguration gotoDomainWhiteListConfiguration; // 短链接跳转白名单的配置类Bean
    private final ShortLinkStatsSaveProducer shortLinkStatsSaveProducer; // redis消息队列生产者

    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain; // 使用配置文件创建的默认域名

    @Override
    // 通过@Transactional:保证在进行insert插入操作时若任意一个出现异常都进行回滚操作
    @Transactional(rollbackFor = Exception.class)
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        verificationWhitelist(requestParam.getOriginUrl());// 确认原始链接是否存在于白名单列表
        String shortLinkSuffix = generateSuffix(requestParam);  // 该方法返回的短链接后缀一定是不重复的
        // 完整短链接---完整短链接是不带协议的（即http://或https://）,由域名和短链接组成（即域名/短链接）
        String fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain)
                .append("/")
                .append(shortLinkSuffix)
                .toString();
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .domain(createShortLinkDefaultDomain)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .favicon(getFavicon(requestParam.getOriginUrl())) // 设置URL的图标链接
                .createdType(requestParam.getCreatedType())
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .describe(requestParam.getDescribe())
                .shortUri(shortLinkSuffix)
                .enableStatus(0)
                // 注意：创建短链接时，由于还未访问，所以设置totalPv、totalUv、totalUip均为0
                .totalPv(0) // 今日页面访问量（浏览量）
                .totalUv(0) // 今日独立访问数（访客数）
                .totalUip(0) // 今日ip数(独立ip数)
                .delTime(0L) // 短链接删除时间戳
                .fullShortUrl(fullShortUrl)
                .build();
        /*等同于
        String fullShortUrl = requestParam.getDomain() + "/" + shortLinkSuffix;
        ShortLinkDO shortLinkDO = BeanUtil.toBean(requestParam, ShortLinkDO.class);
        shortLinkDO.setShortUri(shortLinkSuffix);
        shortLinkDO.setEnableStatus(0);
        // 域名/短链接---即协议后的部分（http://或https://后的内容）
        shortLinkDO.setFullShortUrl(requestParam.getDomain() + "/" + shortLinkSuffix);
        */
        ShortLinkGotoDO shortLinkGoToDo = ShortLinkGotoDO.builder()
                .fullShortUrl(fullShortUrl)
                .gid(requestParam.getGid())
                .build();
        try {
            // 由于数据库中的字段full_short_url具有唯一约束，所以为了避免向数据库中插入相同的完整短链接导致数据库插入失败报DuplicateKeyException异常
            // 所以需要将该代码放在try...catch块中来处理数据库可能抛出的异常---注意：BaseMapper的insert方法在插入失败时一定会抛出异常
            baseMapper.insert(shortLinkDO);
            shortLinkGotoMapper.insert(shortLinkGoToDo); // 将新增的短链接添加到路由表t_link_goto
        } catch (DuplicateKeyException ex) {
            // 插入失败的原因有很多，为什么确定一定是短链接重复了呢？，为什么不在进行一次数据库查询来确定是否是针对存在重复呢？
                // 因为：在利用generateSuffix生成短链接后缀时，已经通过布隆过滤器进行判断了，判断数据库中是否存在该短链接，若存在就重新生成短链接后缀
                // 但是布隆过滤器不是存在一定的误判吗？
                    // 它的误判是会判断生成的短链接后缀存在于数据库中，如果产生误判重新生成一个就行了
            log.warn("短链接 {} 重复入库",fullShortUrl);
            throw new ServiceException(String.format("短链接 : %s 生成重复", fullShortUrl));
                // 短链接不一定存在于数据库
        }
        // 将生成的短链接添加到Redis缓存中并设置有效期---模拟缓存预热
        stringRedisTemplate.opsForValue().set(
                String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                requestParam.getOriginUrl(),
                LinkUtil.getLinkCacheValidDate(requestParam.getValidDate()),
                TimeUnit.MILLISECONDS);
        // 将生成的完整短链接添加到布隆过滤器中
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://" + shortLinkDO.getFullShortUrl())
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .build();
    }

    @Override
    public ShortLinkBatchCreateRespDTO batchCreateShortLink(ShortLinkBatchCreateReqDTO requestParam) {
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes = requestParam.getDescribes();
        List<ShortLinkBaseInfoRespDTO> result = new ArrayList<>();
        for (int i = 0; i < originUrls.size(); i++) {
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParam, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateReqDTO.setDescribe(describes.get(i));
            try {
                ShortLinkCreateRespDTO shortLink = createShortLink(shortLinkCreateReqDTO);
                ShortLinkBaseInfoRespDTO linkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .fullShortUrl(shortLink.getFullShortUrl())
                        .originUrl(shortLink.getOriginUrl())
                        .describe(describes.get(i))
                        .build();
                result.add(linkBaseInfoRespDTO);
            } catch (Throwable ex) {
                log.error("批量创建短链接失败，原始参数：{}", originUrls.get(i));
            }
        }
        return ShortLinkBatchCreateRespDTO.builder()
                .total(result.size())
                .baseLinkInfos(result)
                .build();
    }

    @Override
    // 指定触发事务回滚的异常类型
    @Transactional(rollbackFor = Exception.class)
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        verificationWhitelist(requestParam.getOriginUrl()); // 确认原始链接是否存在于白名单列表
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                // 查询条件必须包含分片键，否则无法从t_link分表中查询成功
                .eq(ShortLinkDO::getGid, requestParam.getOriginGid())
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl()) // 此处查询条件中有完整短链接是因为完整短链接不可被用户修改
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
        // 首先判断是否有该短链接
        if (hasShortLinkDO == null) {
            throw new ClientException("短链接记录不存在");
        }
        // 若存在该短链接则进一步判断数据库中原有短链接的分组标识是否与修改后的分组标识一致
        if (Objects.equals(hasShortLinkDO.getGid(), requestParam.getGid())) { // 分组标识未被更改
            // 修改前后，分组标识一致则直接进行修改
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, requestParam.getOriginGid())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    // 为防止短链接的有效期值为null，所以在进行短链接修改时需要进行判断，若为null，则手动设置有效期为null
                    // 反之则使用原有短链接中的有效期值
                    // 原因：MyBatisPlus会默认忽略null值---将自定义有效期变为永久有效后，数据库表中的对应字段值应为null，而MybatisPlus在更新数据库表时会自动忽略null，所以此处需要进行特殊处理
                    .set(Objects.equals(requestParam.getValidDate(), VailDateTypeEnum.PERMANENT), ShortLinkDO::getValidDate, null);
            // 重新创建一个对象，并将原有信息和要修改的信息存入
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .domain(createShortLinkDefaultDomain)// 注意：域名domain不可被修改，所以使用默认配置的域名即可
                    .shortUri(hasShortLinkDO.getShortUri())
                    .clickNum(hasShortLinkDO.getClickNum())
                    .favicon(hasShortLinkDO.getFavicon())
                    .createdType(hasShortLinkDO.getCreatedType())
                    .enableStatus(hasShortLinkDO.getEnableStatus())//
                    .originUrl(requestParam.getOriginUrl())
                    .fullShortUrl(requestParam.getFullShortUrl())
                    .gid(requestParam.getGid())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .describe(requestParam.getDescribe())
                    .build();
            // 数据库会以updateWrapper为条件更新shortLinkDO对象中不为null的值
            // Wrappers.lambdaUpdate中设置set的原因：若shortLinkDO对象中的有效期为null，此时系统不会自动更改有效期的值
            // 所以为了系统能够修改有效期的值就需要利用set方法来进行设置
            baseMapper.update(shortLinkDO, updateWrapper);
        } else { // 分组标识被更改---即短链接变更分组
            // 添加redisson读写锁(即Redisson实现的分布式读写锁)---注意：读读不互斥，读写互斥---适用于读多写少的场景
            // 保证在读数据时无法写数据
                // LOCK_GID_UPDATE_KEY为短链接修改分组 ID (即分组标识)锁前缀
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, requestParam.getFullShortUrl()));
            RLock rLock = readWriteLock.writeLock(); // 在修改数据时获取写锁（即排他锁）避免在修改数据时可通过其它接口获取数据（即避免前端获取到修改前的数据）
            // 注意：由于在获取数据时需要先获取读锁（即共享锁），而读写锁不能同时获得，所以在工作人员修改数据时需要先去判断是否能够正常获取写锁（即排他锁），若不能则说明数据正在被用户访问,此时已添加了读锁，只能等待访问结束后再去进行修改
            if (!rLock.tryLock()) {
                throw new ServiceException("短链接正在被访问，请稍后再试...");
            }

            try {
                // 短链接分组标识被更改则直接将原数据放入回收站并从回收站逻辑删除
                // 注意:单单只放入回收站的话只用更改启用标识enableStatus为1即可，而从回收站删除则需要处理更改启用标识外还要设置删除标识del_flag为1和删除时间字段delTime
                LambdaUpdateWrapper<ShortLinkDO> linkUpdateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                        .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .eq(ShortLinkDO::getDelTime, 0L)
                        .eq(ShortLinkDO::getEnableStatus, 0);
                ShortLinkDO delShortLinkDO = ShortLinkDO.builder()
                        .delTime(System.currentTimeMillis())
                        .build();
                delShortLinkDO.setDelFlag(1);
                baseMapper.update(delShortLinkDO, linkUpdateWrapper); // 逻辑删除

                // 删除原始数据后重新添加新数据 --- 注意t_link表使用的是联合索引即full_short_url和delTime，所以不用担心出现问题
                ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                        .domain(createShortLinkDefaultDomain)
                        .originUrl(requestParam.getOriginUrl())
                        .gid(requestParam.getGid())
                        .createdType(hasShortLinkDO.getCreatedType())
                        .validDateType(requestParam.getValidDateType())
                        .validDate(requestParam.getValidDate())
                        .describe(requestParam.getDescribe())
                        .shortUri(hasShortLinkDO.getShortUri())
                        .enableStatus(hasShortLinkDO.getEnableStatus())
                        .totalPv(hasShortLinkDO.getTotalPv())
                        .totalUv(hasShortLinkDO.getTotalUv())
                        .totalUip(hasShortLinkDO.getTotalUip())
                        .fullShortUrl(hasShortLinkDO.getFullShortUrl())
                        .favicon(hasShortLinkDO.getFavicon())
                        .delTime(0L)
                        .build();
                baseMapper.insert(shortLinkDO);

                /*
                * 由于更改了分组标识gid，所以要将其它具有分组标识的表的相关短链接数据进行修改
                * 修改方式：
                *   1.针对未分表的情况：直接更新即可
                *   2.针对分表且分片键为gid的情况：直接物理删除然后重新插入
                *   3.针对分表但分片键不是gid的情况：直接物理删除然后重新插入 ---该方式可能有问题，可能直接更新即能解决问题---后续会解决
                *   以下是对各个表的操作，包括
                * t_link_stats_today表（短链接当日访问统计表）
                * t_link_goto表（短链接跳转表）
                * t_link_access_stats(短链接基础访问监控表)
                */

                // 修改t_link_stats_today表（短链接当日访问统计表）---该表通过gid进行分表，由于gid更改，所以先物理删除在插入
                LambdaQueryWrapper<LinkStatsTodayDO> statsTodayQueryWrapper = Wrappers.lambdaQuery(LinkStatsTodayDO.class)
                        .eq(LinkStatsTodayDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkStatsTodayDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkStatsTodayDO::getDelFlag, 0);
                List<LinkStatsTodayDO> linkStatsTodayDOList = linkStatsTodayMapper.selectList(statsTodayQueryWrapper);
                if (CollUtil.isNotEmpty(linkStatsTodayDOList)) {
                    // 通过id列表批量删除原始数据
                    linkStatsTodayMapper.deleteBatchIds(linkStatsTodayDOList.stream()
                            .map(LinkStatsTodayDO::getId)
                            .toList()
                    );
                    // 批量新增修改后的数据
                    // 由于修改时只能修改要跳转的原始链接、描述、短链接分组和有效期，而统计短链接当日访问量的表中并不存在这个几个字段，所以只需要更改批量插入数据的分组标识即可
                    linkStatsTodayDOList.forEach(each -> each.setGid(requestParam.getGid())); // 修改每条数据的gid
                    linkStatsTodayService.saveBatch(linkStatsTodayDOList); // 一次性向数据库中插入多个实体类对象
                }

                // 修改t_link_goto表（短链接跳转表）---该表通过full_short_url分片键进行分表，所以先物理删除在插入---此处有个小问题，可能只需要直接更新即可解决问题，后续会解决
                LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkGotoDO::getGid, hasShortLinkDO.getGid());
                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
                shortLinkGotoMapper.deleteById(shortLinkGotoDO.getId());
                shortLinkGotoDO.setGid(requestParam.getGid());
                shortLinkGotoMapper.insert(shortLinkGotoDO);

                // 修改t_link_access_stats(短链接基础访问监控表)---该表并未分表所以直接更新即可
                LambdaUpdateWrapper<LinkAccessStatsDO> linkAccessStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkAccessStatsDO.class)
                        .eq(LinkAccessStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkAccessStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkAccessStatsDO::getDelFlag, 0);
                LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkAccessStatsMapper.update(linkAccessStatsDO, linkAccessStatsUpdateWrapper);

                // 修改t_link_locale_stats(访问地区统计监控表)---该表并未分表所以直接更新即可
                LambdaUpdateWrapper<LinkLocaleStatsDO> linkLocaleStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkLocaleStatsDO.class)
                        .eq(LinkLocaleStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkLocaleStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkLocaleStatsDO::getDelFlag, 0);
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkLocaleStatsMapper.update(linkLocaleStatsDO, linkLocaleStatsUpdateWrapper);

                // 修改t_link_os_stats(OS统计监控表)---该表并未分表所以直接更新即可
                LambdaUpdateWrapper<LinkOsStatsDO> linkOsStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkOsStatsDO.class)
                        .eq(LinkOsStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkOsStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkOsStatsDO::getDelFlag, 0);
                LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkOsStatsMapper.update(linkOsStatsDO, linkOsStatsUpdateWrapper);

                // 修改t_link_browser_stats（浏览器统计监控表）---该表并未分表所以直接更新即可
                LambdaUpdateWrapper<LinkBrowserStatsDO> linkBrowserStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkBrowserStatsDO.class)
                        .eq(LinkBrowserStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkBrowserStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkBrowserStatsDO::getDelFlag, 0);
                LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkBrowserStatsMapper.update(linkBrowserStatsDO, linkBrowserStatsUpdateWrapper);

                // 修改t_link_device_stats（访问设备监控表）---该表并未分表所以直接更新即可
                LambdaUpdateWrapper<LinkDeviceStatsDO> linkDeviceStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkDeviceStatsDO.class)
                        .eq(LinkDeviceStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkDeviceStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkDeviceStatsDO::getDelFlag, 0);
                LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkDeviceStatsMapper.update(linkDeviceStatsDO, linkDeviceStatsUpdateWrapper);

                // 修改t_link_network_stats（访问网络监控表）---该表并未分表所以直接更新即可
                LambdaUpdateWrapper<LinkNetworkStatsDO> linkNetworkStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkNetworkStatsDO.class)
                        .eq(LinkNetworkStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkNetworkStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkNetworkStatsDO::getDelFlag, 0);
                LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkNetworkStatsMapper.update(linkNetworkStatsDO, linkNetworkStatsUpdateWrapper);

                // 修改t_link_access_logs(短链接访问日志监控表)---该表并未分表所以直接更新即可
                LambdaUpdateWrapper<LinkAccessLogsDO> linkAccessLogsUpdateWrapper = Wrappers.lambdaUpdate(LinkAccessLogsDO.class)
                        .eq(LinkAccessLogsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkAccessLogsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkAccessLogsDO::getDelFlag, 0);
                LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkAccessLogsMapper.update(linkAccessLogsDO, linkAccessLogsUpdateWrapper);
            } finally {
                rLock.unlock();//释放锁
            }
        }
        // 解决短链接修改后，有效期过期仍然可以访问的问题
        if (!Objects.equals(hasShortLinkDO.getValidDateType(), requestParam.getValidDateType())
                || !Objects.equals(hasShortLinkDO.getValidDate(), requestParam.getValidDate())) {
            stringRedisTemplate.delete(String.format(GOTO_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
            // 解决将失效短链接变更为有效短链接后仍不可跳转原始链接的问题
                // 判断是否是已经过期的短链接
            if (hasShortLinkDO.getValidDate() != null && hasShortLinkDO.getValidDate().before(new Date())) {
                // 判断是否是将已过期的短链接变更为有效短链接
                if (Objects.equals(requestParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()) || requestParam.getValidDate().after(new Date())) {
                    // 删除短链接存在于数据库的Redis缓存标志
                    stringRedisTemplate.delete(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
                }
            }
        }
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
        IPage<ShortLinkDO> resultPage = baseMapper.pageLink(requestParam);
        return resultPage.convert(each -> {
            ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            result.setDomain("http://" + result.getDomain());
            return result;
        });
    }

    @Override
    public List<ShortLinkGroupCountQueryRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        /*
        * 注意：此处虽然未设置createdType以及validDateType的值，但是生成的sql语句会自动添加这两个字段，且值默认为0，因为它俩的类型为int
        * 因此为了避免该情况需将这两个字段设置为Integer类型，否则查询会失败
        * */
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                .select("gid as gid, count(*) as shortLinkCount")
                .in("gid", requestParam)
                .eq("enable_status", 0)
                .eq("del_flag", 0)
                .eq("del_time", 0L)
                .groupBy("gid");
        List<Map<String, Object>> shortLinkDOList = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(shortLinkDOList, ShortLinkGroupCountQueryRespDTO.class);
    }

    /**
     * 测试时在hosts文件中输入127.0.0.1 nurl.ink，然后打开浏览器输入nurl.ink:8001/2f3CWd回车即可跳转到https://nageoffer.com/这个网站
     * 若想测试短链接对应的原始链接不存在则直接重定向到“页面不存在”的页面，则输入一个nurl.ink:8001/12QWER测试即可
     * hosts文件位置C:\Windows\System32\drivers\etc
     */
    @SneakyThrows // 忽略异常的注解
    @Override
    public void restoreUrl(String shortUri, HttpServletRequest request, HttpServletResponse response) {
        String serverName = request.getServerName();// 返回客户端请求的服务器名称，即服务器的主机名(即域名)或 IP 地址。
        // 判断短链接跳转时前端传来的端口号是否不是80，若不是则使用前端的端口号（即在配置文件中配置的端口号8001），反之将端口号设置为空字符串（即""）
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(String::valueOf)
                .map(each -> ":" + each)
                .orElse("");
        /*
        String httpOrHttps = request.getScheme(); // 获取前端请求的当前协议（http还是https）---若前端未加协议则默认为http
        String fullShortUrl = StrBuilder.create(httpOrHttps)
                .append("://")
                .append(serverName)
                .append(serverPort)
                .append("/")
                .append(shortUri)
                .toString();
                等同于 String fullShortUrl = httpOrHttps + "://" + serverName + serverPort + "/" + shortUri;
                */
        // 注意：由于本系统在开发过程中存入到数据库的完整短链接并未添加协议，所以以上代码改为如下代码
        String fullShortUrl = StrBuilder.create(serverName)
                .append(serverPort)
                .append("/")
                .append(shortUri)
                .toString();
        // 解决缓存击穿和缓存穿透
        // 判断Redis缓存中是否存在
        String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(originalLink)) {
            // 短链接基础访问记录统计---注意：必须在重定向之前，否则Cookie在加入Redis缓存之前就通过response返回给前端了，导致Cookie添加失败
            ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(fullShortUrl, null, statsRecord);
            // Redis缓存中存在---则不会产生缓存击穿、缓存穿透、缓存雪崩这三个问题
            // 将客户端的请求重定向到另一个 URL
            response.sendRedirect(originalLink);
            return;
        }

        /* Redis缓存中不存在---则可能产生缓存击穿、缓存穿透、缓存雪崩
         * 缓存击穿：某个热点数据在缓存中不存在，但在数据库中存在
         * 缓存穿透：某个热点数据在缓存中不存在，且在数据库中也不存在
         * 缓存雪崩：
         */
        // 判断布隆过滤器中是否存在---用于解决缓存穿透---只有确定了Redis和布隆过滤器中均不存在后才能确保会产生缓存穿透
        boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);
        if (!contains) {
            // 布隆过滤器中不存在代表数据库中不存在---则直接重定向到“页面不存在”的页面，避免缓存穿透
            response.sendRedirect("/page/notfound");
            return;
        }
        // 布隆过滤器中存在---则说明数据库中存在该热点数据，因此会产生缓存击穿、缓存雪崩
        // 为避免布隆过滤器误判，需要进一步通过Redis缓存来判断数据库是否存在该短链接对应的链接的值
        String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
            // 若Key对应的值不为空则代表不存在误判，数据库中确实不存在该数据，因此会产生缓存穿透---直接重定向到“页面不存在”的页面，避免缓存穿透
            response.sendRedirect("/page/notfound");
            return;
        }
        // Redis中不存在、布隆过滤器中存在---可能是缓存击穿、缓存雪崩
        //  Redis 分布式锁--- Redis的框架Redisson所提供的分布式可重入锁RLock---底层有一个看门狗机制
        // redission锁有看门狗的机制，所以实际项目中尽量使用redission分布式锁 --- 可用来解决缓存击穿---适用于需要强一致性的场景
        RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));// 分布式锁
        lock.lock();
        try {
            originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
            // 该if 为双重判定锁---作用：保证只有第一个线程从数据库中获取相关数据，而后续获取到锁的线程不必在从数据库中获取
            if (StrUtil.isNotBlank(originalLink)) {
                // 将客户端的请求重定向到另一个 URL
                response.sendRedirect(originalLink);
                return;
            }
            LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO shortLinkGoToDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
            if (shortLinkGoToDO == null) {
                // 数据库中不存在相关数据，则在Redis缓存中设置该数据Key对应的值为-，且过期时间为30min,来避免布隆过滤器误判，避免缓存穿透
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                // 数据库中不存在相关数据---直接重定向到“页面不存在”的页面，避免缓存穿透
                response.sendRedirect("/page/notfound");
                return;
            }
            // 若数据库中存在相关数据，且
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, shortLinkGoToDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0); // 排除移到回收站的短链接
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
            // shortLinkDO.getValidDate() != null代表不是永久有效期,而是用户自定义的有效期，所以还需要判断自定义有效期是否在当前时间之前，若在则代表已过期----数据已不可用，不可在获取
            if (shortLinkDO == null || (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date()))) {
                // 短链接过期---在缓存中设置一个标志，避免缓存穿透
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                // 短链接过期---直接重定向到“页面不存在”的页面，避免缓存穿透
                response.sendRedirect("/page/notfound");
                return;
            }
            // 数据在缓存中未过期
            // 将数据库中的短链接直接添加到Redis缓存中并设置有效期---模拟缓存预热
            stringRedisTemplate.opsForValue().set(
                    String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                    shortLinkDO.getOriginUrl(),
                    LinkUtil.getLinkCacheValidDate(shortLinkDO.getValidDate()),
                    TimeUnit.MILLISECONDS);
            // 短链接基础访问记录统计---注意：必须在重定向之前，否则Cookie在加入Redis缓存之前就通过response返回给前端了，导致Cookie添加失败
            ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(fullShortUrl, null, statsRecord);
            // 将客户端的请求重定向到另一个 URL
            response.sendRedirect(shortLinkDO.getOriginUrl());
        } finally {
            lock.unlock();
        }
    }

    /**
     * 利用工具类生成短链接后缀
     * @param requestParam 创建短链接请求参数
     * @return 生成的短链接
     */
    private String generateSuffix(ShortLinkCreateReqDTO requestParam) {
        // 利用循环解决生成的短链接冲突，同时设置最大重复次数
        int customGenerateCount = 0;
        String shortUri = null;
        while (true) {
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接重复生成，请稍后在试");
            }
            String originUrl = requestParam.getOriginUrl();
            // 将原始链接加上当前毫秒数,避免生成的短链接冲突---即降低哈希冲突的概率
                // 此处加上当前毫秒数的链接只用于生成短链接，并不会影响原始链接，因为最后仍会将前端传来的原始链接添加到数据库中，而不是加上毫秒数的原始链接
                // 注意：加上当前毫秒数也会产生问题：若大量请求在统一毫秒内创建短链接，此时就会导致originUrl+System.currentTimeMillis()后仍然一致
            // originUrl += System.currentTimeMillis();
            // 所以最好使用originUrl + UUID来生成短链接

            originUrl += UUID.randomUUID().toString();
            shortUri = HashUtil.hashToBase62(originUrl);
            if (!shortUriCreateCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain + "/" + shortUri)) {
                break;
            } else {
                customGenerateCount++;
            }
        }
        return shortUri;
    }

    /**
     * 确认原始链接是否存在于白名单列表
     * @param originUrl
     */
    private void verificationWhitelist(String originUrl) {
        Boolean enable = gotoDomainWhiteListConfiguration.getEnable(); // 获取是否开启白名单验证功能的配置属性
        if (enable == null || !enable) {
            return; // 未开启白名单验证功能则直接返回即可，代表用户可创建任何网站的短链接---即此时未开启封控功能
        }
        // 若开启了白名单验证功能则需要获取其域名并判断该域名是否在白名单之中
        String domain = LinkUtil.extractDomain(originUrl);
        if (StrUtil.isBlank(domain)) {
            // 若域名为空则说明跳转链接填写错误，返回客户端异常给前端，由前端提醒用户
            throw new ClientException("跳转链接填写错误");
        }
        // 获取配置文件白名单中的域名
        List<String> details = gotoDomainWhiteListConfiguration.getDetails();
        if (!details.contains(domain)) {
            // 若用户要创建短链接的域名不在白名单范围内，则返回客户端异常给前端，由前端提醒用户
            throw new ClientException("演示环境为避免恶意攻击，请生成以下网站跳转链接：" + gotoDomainWhiteListConfiguration.getNames());
        }
    }

    /**
     * 获取网站的favicon图标链接
     * @param url 网站URL
     * @return favicon图标链接，若不存在则直接返回null
     */
    @SneakyThrows
    private String getFavicon(String url){
        // 创建URL对象
        URL targetUrl = new URL(url);
        // 打开URL
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        // 禁止自动处理重定向
        connection.setInstanceFollowRedirects(false);
        // 设置请求方法
        connection.setRequestMethod("GET");
        // 连接
        connection.connect();
        // 获取响应码
        int responseCode = connection.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
            // 若响应码是重定向响应码则获取重定向的URL，并重新进行连接
            String redirectUrl = connection.getHeaderField("Location");
            if (redirectUrl != null) {
                URL newUrl = new URL(redirectUrl);
                connection = (HttpURLConnection) newUrl.openConnection();
                connection.setRequestMethod("GET");
                connection.connect();
                // 获取重新连接后的响应码
                responseCode = connection.getResponseCode();
            }
        }
        if (responseCode == HttpURLConnection.HTTP_OK) {
            // 若响应码是200，则使用Jsoup库连接到URL并获取文档对象
            Document document = Jsoup.connect(url).get();
            // 选择第一个匹配的<link>标签，其rel属性包含"shortcut"或"icon"
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                // 若存在图片连接则返回该图标链接的绝对路径
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }

    /**
     * 短链接基础访问记录统计
     * 待解决的问题：如何保证大数据量的ip、cookie不会将Redis撑爆
     */
    private ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetUser(String fullShortUrl, HttpServletRequest request, HttpServletResponse response) {
        AtomicBoolean uvFirstFlag = new AtomicBoolean(); // 由于使用的是Lambda表达式，所以此处用原子类AtomicBoolean代替boolean
        Cookie[] cookies = request.getCookies();
        // AtomicReference<V> 是 Java 中的一种原子类,能够保证对某个对象引用的原子性操作,在多线程下是线程安全的---此处用来操作String对象.
        // 此处使用原子类是因为lambad表达式不能使用普通数据类型
        // AtomicReference<V>针对引用类型（如对象）提供原子操作
        AtomicReference<String> uv = new AtomicReference<>();
        // 定义一个Runnable任务（即实现Runnable接口的匿名实现类，可用来开启子线程）---设置Cookie并发送给前端
        Runnable addResponseCookieTask = () -> {
            uv.set(UUID.fastUUID().toString()); // 生成UUID，格式为：xxx-xxx-xxx-xxx-xxx
            Cookie uvCookie = new Cookie("uv", uv.get()); // 创建Cookie对象并存入Cookie数据
            // 设置Cookie的最大存活时间（即有效期）为30天---原因：此时会将Cooke写入浏览器所在电脑的硬盘，持久化存储，以此来判断是否是同一个访客来访问
            // 注意：Cookie 是存储在每个浏览器的本地存储中，不同的浏览器之间是相互独立的，它们不能共享存储的 Cookie
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            // StrUtil.sub方法实现对完整短链接的截取，此处是获取短链接shortUri
                // 设定浏览器在指定uri下才会发送Cookie到服务端---若不设置则默认为浏览器发送的所有请求都会带Cookie
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            response.addCookie(uvCookie); // 将Cookie发送到前端
            uvFirstFlag.set(Boolean.TRUE);
            // 注意：此处有问题，若有千万级用户来访问，此时就会向Redis中存入大量的Cookie缓存，这是一个弊端，后续会解决
            stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + fullShortUrl, uv.get());
        };
        if (ArrayUtil.isNotEmpty(cookies)) {
            // 将 cookies 数组转换为流
            Arrays.stream(cookies)
                    // 过滤出名为 "uv" 的 Cookie
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    // 找到第一个符合条件的 Cookie
                    .findFirst()
                    .map(Cookie::getValue) // 获取第一个符合条件的 Cookie 的值（即之前保存的 "uv"）
                    // 若第一个第一个符合条件的 Cookie 有值则执行第一个参数---即将该值保存到Redis缓存中，并设置uvFirstFlag为1，代表有值；反之则执行第二个参数---即执行Runnable任务（注意，ifPresentOrElse方法内部会调用Runnable.run来执行Runnable任务）
                    .ifPresentOrElse(each -> {
                        uv.set(each); // 更新前端用户的Cookie
                        // 向Redis缓存的set集合中添加元素时，若添加成功则会返回添加成功的数量，若失败则会返回0
                        Long uvAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + fullShortUrl, each);// 将返回的long类型自动封装为包装类--若each 是第一次被添加到集合中，返回 1L;反之，返回0L
                        uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                    }, addResponseCookieTask); // ifPresentOrElse方法中会直接调用run()方法在当前线程中运行
        } else {
            addResponseCookieTask.run(); // 由于直接调用run方法，所以代表在当前线程下运行；若调用start()方法则代表开启一个子线程运行
        }

        // 获取用户真实IP地址---注意：此处有问题，若有千万级用户来访问，此时就会向Redis中存入大量的IP缓存，这是一个弊端，后续会解决
        String remoteAddr = LinkUtil.getActualIp(request);
        String os = LinkUtil.getOs(request);
        String browser = LinkUtil.getBrowser(request);
        String device = LinkUtil.getDevice(request);
        String network = LinkUtil.getNetwork(request);
        // 将返回的long类型自动封装为包装类--若each 是第一次被添加到集合中，返回 1L;反之，返回0L
        Long uipAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UIP_KEY + fullShortUrl, remoteAddr);
        boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;// 若值为true代表第一次访问，是一个新的ip；反之则不是第一次访问
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .uv(uv.get())
                .uvFirstFlag(uvFirstFlag.get()) // 记录该访客是否是第一次访问该短链接
                .uipFirstFlag(uipFirstFlag)     // 记录访问该短链接的ip是否是新ip：若值为true代表第一次访问，是一个新的ip；反之则不是第一次访问
                .remoteAddr(remoteAddr) // 访问的ip地址
                .os(os)                 // 访问的操作系统os
                .browser(browser)       // 访问的浏览器
                .device(device)         // 访问的设备
                .network(network)       // 访问的网络
                .build();
    }

    /**
     * 短链接基础访问记录统计
     * 待解决的问题：如何保证大数据量的ip、cookie不会将Redis撑爆
     */
    @Override
    public void shortLinkStats(String fullShortUrl, String gid, ShortLinkStatsRecordDTO statsRecord) {
        // 集合封装消息内容，用来统计短链接基础访问记录
        Map<String, String> producerMap = new HashMap<>();
        producerMap.put("fullShortUrl", fullShortUrl);
        producerMap.put("gid", gid);
        producerMap.put("statsRecord", JSON.toJSONString(statsRecord));
        shortLinkStatsSaveProducer.send(producerMap); // 通过Redis Stream的消息队列生产者将其发送到消息队列中, 作用：避免突然有大量请求导致数据库崩溃
    }
}